package main;

import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.PixelFormat;
import org.lwjgl.input.Keyboard;
import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;

/**
 * Base class for OpenGL examples. Provides a framework in form of a continuous loop and several
 * protected functions that are intended to be overridden to provide custom behaviour. Also
 * initialises display and common settings. 
 * 
 * @author Ciardhubh
 */
public class Base {

    /*
     * Properties of the display window. They are set by the constructors. Width and height are
     * visible to extending classes.
     */
    private final String WINDOW_TITLE;
    protected final int WINDOW_WIDTH;
    protected final int WINDOW_HEIGHT;
    private final int MAX_REFRESH_RATE;
    private final int MIN_BITS_PER_PIXEL;
    private final boolean VSYNC;
    private final boolean FULLSCREEN;
    /** Ratio of WINDOW_WIDTH / WINDOW_HEIGHT. */
    protected final float RATIO;
    /*
     * Static default values for constructors which don't have all parameters.
     */
    private static final String DEFAULT_TITLE = "OpenGL with LWJGL";
    private static final int DEFAULT_WINDOW_WIDTH = 640;
    private static final int DEFAULT_WINDOW_HEIGHT = 480;
    private static final int DEFAULT_MAX_REFRESH_RATE = Display.getDisplayMode().getFrequency();
    private static final int DEFAULT_MIN_BITS_PER_PIXEL = Display.getDisplayMode().getBitsPerPixel();
    private static final boolean DEFAULT_VSYNC = false;
    private static final boolean DEFAULT_FULLSCREEN = false;
    /** Time at the start of the loop. */
    private long now = System.currentTimeMillis();
    /** Last time the main loop has been run. */
    private long lastTime = now;
    /** Time since last loop run. */
    private long timeDelta = 0;
    /** Pixel format used by the display. If it is null, the LWJGL default will be used. */
    private PixelFormat pixelFormat = null;
    /** If set to true, the application will exit at the start of the next loop iteration. */
    private boolean exit = false;

    /**
     * Creates a new Base with default window dimensions and title.
     */
    protected Base() {
        this(DEFAULT_TITLE, DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT);
    }

    /**
     * Creates a new Base with the given window dimensions and title.
     * 
     * @param windowTitle Window title.
     * @param windowWidth Width in pixels.
     * @param windowHeight Height in pixels.
     */
    public Base(String windowTitle, int windowWidth, int windowHeight) {
        this(windowTitle, windowWidth, windowHeight, DEFAULT_MAX_REFRESH_RATE,
                DEFAULT_MIN_BITS_PER_PIXEL, DEFAULT_VSYNC, DEFAULT_FULLSCREEN);
    }

    /**
     * Sets the PixelFormat that is to be used by the display. Calls to this function are only
     * effective if called before go(). The default pixel format is used if null is given as
     * parameter or if this function is not called at all.
     * 
     * @param pixelFormat Display's PixelFormat or null if LWJGL default is to be used.
     */
    protected void setPixelFormat(PixelFormat pixelFormat) {
        this.pixelFormat = pixelFormat;
    }

    /**
     * Creates a new Base with the given display properties.
     * 
     * @param windowTitle Window title.
     * @param windowWidth Width in pixels.
     * @param windowHeight Height in pixels.
     * @param maxRefreshRate Refresh rate of the display mode in hz.
     * @param minBitsPerPixel Bits per pixel
     * @param vsync Turns vsync on or off.
     * @param fullscreen Turns fullscreen on or off.
     */
    protected Base(String windowTitle, int windowWidth, int windowHeight, int maxRefreshRate,
            int minBitsPerPixel, boolean vsync, boolean fullscreen) {
        WINDOW_TITLE = windowTitle;
        WINDOW_WIDTH = windowWidth;
        WINDOW_HEIGHT = windowHeight;
        MAX_REFRESH_RATE = maxRefreshRate;
        MIN_BITS_PER_PIXEL = minBitsPerPixel;
        VSYNC = vsync;
        FULLSCREEN = fullscreen;
        RATIO = (float) WINDOW_WIDTH / (float) WINDOW_HEIGHT;

        // FIXME Workaround for the Sys.alert(...)-not-showing bug.
        java.awt.Toolkit.getDefaultToolkit();
    }

    /**
     * Starts processing. Initialises OpenGL and calls the run() method. Before exiting, cleanup()
     * is called.
     */
    public void go() {
        try {
            initDisplay();
            init();
            run();
        } catch (LWJGLException ex) {
            System.out.println(ex.toString());
            Sys.alert(WINDOW_TITLE, "An exception occured. Exiting application.");
        } finally {
            cleanup();
        }
    }

    /**
     * Initialises the OpenGL display.
     * 
     * @throws org.lwjgl.LWJGLException
     */
    protected void initDisplay() throws LWJGLException {
        // Set display mode.
        DisplayMode[] modes = Display.getAvailableDisplayModes();
        for (DisplayMode m : modes) {
            if (m.getWidth() == WINDOW_WIDTH &&
                    m.getHeight() == WINDOW_HEIGHT &&
                    m.getBitsPerPixel() >= MIN_BITS_PER_PIXEL &&
                    m.getFrequency() <= MAX_REFRESH_RATE) {
                Display.setDisplayMode(m);
            }
        }
        // Set various properties.
        Display.setTitle(WINDOW_TITLE);
        Display.setFullscreen(FULLSCREEN);
        Display.setVSyncEnabled(VSYNC);

        if (pixelFormat == null) {
            Display.create();
        } else {
            Display.create(pixelFormat);
        }
    }

    /**
     * Initialises OpenGL drawing settings.
     */
    protected void init() {
        GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        GL11.glColor3f(1.0f, 1.0f, 1.0f);
    }

    /**
     * Does the actual drawing work. Executed after initialisation. Runs a continuous loop that
     * calls logic() and render().
     */
    protected void run() {
        while (!Keyboard.isKeyDown(Keyboard.KEY_ESCAPE) && !Display.isCloseRequested() && !exit) {
            now = System.currentTimeMillis();
            timeDelta = now - lastTime;
            lastTime = now;
            if (Display.isVisible()) {
                logic(timeDelta);
                render();
            } else {
                if (Display.isDirty()) {
                    render();
                }
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ex) {
                }
            }
            Display.update();
        }
    }

    /**
     * Creates a new image that is to be displayed.
     */
    protected void render() {
    }

    /**
     * Handles logic that does not concern rendering, e.g. animation, keyboard or mouse input.
     * 
     * @param timeDelta Time in milliseconds since the last time the loop has run; used in animation
     * to determine how much an animated object has to be moved.
     */
    protected void logic(long timeDelta) {
    }

    /**
     * Executed before the application exits to clean up the display, etc.
     */
    protected void cleanup() {
        if (Display.isCreated()) {
            Display.destroy();
        }

    }

    /**
     * Sets a flag to exit the application at the start of the next loop iteration.
     */
    protected void exit() {
        exit = true;
    }
}
